Almost every app you use talks to a server through an API, and most of those APIs follow the REST style: resources live at URLs, and you act on them with standard HTTP methods. This guide explains the ideas you need, shows the methods and status codes in tables, then builds the same small books API twice, once in Python with Flask and once in Node.js with Express. Finally it calls the API with curl and with JavaScript's fetch. All the code was run and every response shown below is real output.
Versions and facts in this guide were checked against official documentation on 26 October 2026 (IST). Tools move fast, so confirm version numbers on the linked official pages before you install.
What a REST API is
REST (Representational State Transfer) is an architectural style, not a library or a standard you install. In practice, a "REST API" or "RESTful API" usually means:
- Resources have URLs.
/booksis the collection of books;/books/2is one book. - HTTP methods say what to do. GET reads, POST creates, PUT replaces, PATCH partially updates, DELETE removes.
- Status codes say what happened. 200 OK, 201 Created, 404 Not Found and so on.
- Data travels as JSON in request and response bodies, with a
Content-Type: application/jsonheader. - Each request is stateless. It carries everything the server needs (including authentication, such as a token header). The server does not rely on remembering earlier requests.
HTTP methods
| Method | Purpose | Example | Safe? | Idempotent? |
|---|---|---|---|---|
| GET | Read a resource or list | GET /books | Yes | Yes |
| POST | Create a new resource | POST /books | No | No |
| PUT | Replace a resource completely | PUT /books/2 | No | Yes |
| PATCH | Change part of a resource | PATCH /books/2 | No | Not necessarily |
| DELETE | Remove a resource | DELETE /books/2 | No | Yes |
Safe means the request should not change anything on the server. Idempotent means sending the same request twice has the same effect on the server as sending it once. Deleting book 2 twice leaves it deleted, while POSTing twice creates two books. These definitions come from the HTTP specification, RFC 9110. For making POST safe to retry, see our guide to idempotent API endpoints.
Status codes you will use most
| Code | Meaning | Typical use |
|---|---|---|
| 200 OK | Success, with a body | GET, PUT or PATCH that returns data |
| 201 Created | A new resource was created | Successful POST |
| 204 No Content | Success, no body | Successful DELETE |
| 400 Bad Request | The request is invalid | Missing or malformed fields |
| 401 Unauthorized | Not authenticated | Missing or bad token |
| 403 Forbidden | Authenticated, but not allowed | User lacks permission |
| 404 Not Found | No such resource | Unknown ID |
| 409 Conflict | Clashes with current state | Duplicate or version conflict |
| 422 Unprocessable Content | Well-formed but semantically invalid | Used by some frameworks for validation errors |
| 500 Internal Server Error | Bug or failure on the server | Unhandled exception |
Classes: 2xx success, 3xx redirection, 4xx client error, 5xx server error. A consistent JSON error body (we use {"error": "..."}) makes clients much easier to write. Our article on designing clear API error responses goes deeper.
Designing the books API
| Method and path | Action | Success | Errors |
|---|---|---|---|
GET /books | List all books | 200 | |
GET /books/:id | Get one book | 200 | 404 |
POST /books | Create a book | 201 | 400 |
PUT /books/:id | Replace a book | 200 | 400, 404 |
DELETE /books/:id | Delete a book | 204 | 404 |
Data is kept in memory to keep the focus on HTTP. A real API would use a database (see SQL basics) and authentication.
Version 1: Python with Flask
Install Flask (Flask 3.1.3 was the latest release on PyPI when we tested) in a virtual environment and save the file as app.py:
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install flask# app.py - a minimal REST API for books (Flask 3)
from flask import Flask, jsonify, request
app = Flask(__name__)
books = {
1: {"id": 1, "title": "Wings of Fire", "author": "A. P. J. Abdul Kalam"},
2: {"id": 2, "title": "The Guide", "author": "R. K. Narayan"},
}
next_id = 3
@app.get("/books")
def list_books():
return jsonify(list(books.values()))
@app.get("/books/<int:book_id>")
def get_book(book_id):
book = books.get(book_id)
if book is None:
return jsonify(error="Book not found"), 404
return jsonify(book)
@app.post("/books")
def create_book():
global next_id
data = request.get_json(silent=True) or {}
if not data.get("title") or not data.get("author"):
return jsonify(error="title and author are required"), 400
book = {"id": next_id, "title": data["title"], "author": data["author"]}
books[next_id] = book
next_id += 1
return jsonify(book), 201
@app.put("/books/<int:book_id>")
def update_book(book_id):
if book_id not in books:
return jsonify(error="Book not found"), 404
data = request.get_json(silent=True) or {}
if not data.get("title") or not data.get("author"):
return jsonify(error="title and author are required"), 400
books[book_id] = {"id": book_id, "title": data["title"], "author": data["author"]}
return jsonify(books[book_id])
@app.delete("/books/<int:book_id>")
def delete_book(book_id):
if books.pop(book_id, None) is None:
return jsonify(error="Book not found"), 404
return "", 204
if __name__ == "__main__":
app.run(debug=True, port=5000)Run it with python app.py. The API listens on http://localhost:5000. The @app.get and @app.post decorators map methods and paths to functions, <int:book_id> converts the URL segment to an integer, and returning a tuple such as (body, 201) sets the status code. debug=True is for local development only.
Version 2: Node.js with Express
Create a project and install Express (5.3.0 was the latest when we tested):
mkdir books-express && cd books-express
npm init -y
npm install express// server.js - the same books API in Express 5
const express = require("express");
const app = express();
app.use(express.json());
const books = new Map([
[1, { id: 1, title: "Wings of Fire", author: "A. P. J. Abdul Kalam" }],
[2, { id: 2, title: "The Guide", author: "R. K. Narayan" }],
]);
let nextId = 3;
app.get("/books", (req, res) => {
res.json([...books.values()]);
});
app.get("/books/:id", (req, res) => {
const book = books.get(Number(req.params.id));
if (!book) return res.status(404).json({ error: "Book not found" });
res.json(book);
});
app.post("/books", (req, res) => {
const { title, author } = req.body ?? {};
if (!title || !author) {
return res.status(400).json({ error: "title and author are required" });
}
const book = { id: nextId++, title, author };
books.set(book.id, book);
res.status(201).json(book);
});
app.put("/books/:id", (req, res) => {
const id = Number(req.params.id);
if (!books.has(id)) return res.status(404).json({ error: "Book not found" });
const { title, author } = req.body ?? {};
if (!title || !author) {
return res.status(400).json({ error: "title and author are required" });
}
const book = { id, title, author };
books.set(id, book);
res.json(book);
});
app.delete("/books/:id", (req, res) => {
if (!books.delete(Number(req.params.id))) {
return res.status(404).json({ error: "Book not found" });
}
res.status(204).end();
});
app.listen(3000, () => console.log("API running on http://localhost:3000"));Run it with node server.js. It listens on port 3000. express.json() parses JSON request bodies into req.body, and res.status(...).json(...) sets the status code and sends JSON. The ?? {} guard handles requests that arrive without a JSON body.
Testing with curl
These commands work against either server. Use port 5000 for Flask and 3000 for Express. The responses below are Express's (compact JSON). Flask returns the same data, pretty-printed in debug mode and with keys sorted alphabetically.
# List all books
curl -s http://localhost:3000/books[{"id":1,"title":"Wings of Fire","author":"A. P. J. Abdul Kalam"},{"id":2,"title":"The Guide","author":"R. K. Narayan"}]# A book that does not exist: look at the status line
curl -s -i http://localhost:3000/books/42 | head -1HTTP/1.1 404 Not Found# Create a book (-w prints the status code)
curl -s -X POST http://localhost:3000/books \
-H "Content-Type: application/json" \
-d '{"title": "Godan", "author": "Munshi Premchand"}' \
-w ' %{http_code}\n'{"id":3,"title":"Godan","author":"Munshi Premchand"} 201# Missing author: validation error
curl -s -X POST http://localhost:3000/books \
-H "Content-Type: application/json" \
-d '{"title": "No author"}' -w ' %{http_code}\n'{"error":"title and author are required"} 400# Replace book 2
curl -s -X PUT http://localhost:3000/books/2 \
-H "Content-Type: application/json" \
-d '{"title": "The Guide", "author": "R.K. Narayan"}' -w ' %{http_code}\n'
# Delete book 1, then try again
curl -s -X DELETE http://localhost:3000/books/1 -o /dev/null -w '%{http_code}\n'
curl -s -X DELETE http://localhost:3000/books/1 -w ' %{http_code}\n'{"id":2,"title":"The Guide","author":"R.K. Narayan"} 200
204
{"error":"Book not found"} 404The second DELETE returns 404 because the book is already gone, but the server's state is the same either way. That is idempotency in action. On Windows PowerShell, use curl.exe and adjust the quoting, or use a tool such as Postman or the VS Code REST Client extension.
Calling the API with fetch
fetch is built into every modern browser and into Node.js 18 and later. Save this as client.mjs:
// client.mjs - call the API with fetch (Node 18+ or any modern browser)
const BASE = process.env.API_URL ?? "http://localhost:5000";
async function main() {
// Create a book
const res = await fetch(`${BASE}/books`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "Malgudi Days", author: "R. K. Narayan" }),
});
if (!res.ok) throw new Error(`POST failed: ${res.status}`);
const created = await res.json();
console.log("Created:", res.status, created);
// List all books
const list = await fetch(`${BASE}/books`).then((r) => r.json());
console.log("Total books:", list.length);
// Ask for a book that does not exist
const missing = await fetch(`${BASE}/books/999`);
console.log("Missing book status:", missing.status, await missing.json());
}
main().catch((err) => console.error(err));Running node client.mjs against the Flask server printed:
Created: 201 { author: 'R. K. Narayan', id: 4, title: 'Malgudi Days' }
Total books: 3
Missing book status: 404 { error: 'Book not found' }Two things trip up beginners. First, fetch only rejects on network failures. A 404 or 500 still resolves, so check res.ok or res.status yourself, as the code does. Second, if you call the API from a web page served on a different origin (a different port counts), the browser enforces CORS. The server must send an Access-Control-Allow-Origin header, for example via the flask-cors package or the cors middleware for Express. curl and Node scripts are not affected.
Flask vs Express for this API
| Flask (Python) | Express (Node.js) | |
|---|---|---|
| Routing | Decorators: @app.get("/books") | Methods: app.get("/books", handler) |
| URL parameters | <int:book_id>, converted for you | :id, a string you convert |
| JSON body | request.get_json() | express.json() middleware, then req.body |
| Setting status | Return (body, 201) | res.status(201).json(body) |
| Production server | A WSGI server such as gunicorn | Node itself, often behind a reverse proxy |
If you prefer type hints and automatic OpenAPI docs in Python, FastAPI is a popular alternative to Flask. The REST concepts are identical.
Next steps
- Package the API in a container with our Docker basics guide, which builds a Flask API backed by PostgreSQL.
- Replace the in-memory dictionary with a real database table, and add paging (limit and offset, or cursors) once lists grow large.
- New to Python or JavaScript? Start with Python vs JavaScript vs Java.
FAQ
Is REST the same as HTTP?
No. HTTP is the protocol; REST is a design style that uses HTTP's methods, URLs and status codes in a consistent way.
When should I use PUT vs PATCH?
PUT replaces the whole resource with what you send. PATCH applies a partial change. Our example uses PUT and requires both title and author.
REST vs GraphQL?
REST exposes several resource URLs. GraphQL usually exposes one endpoint where the client asks for exactly the fields it needs. REST is simpler to start with and is still the most common style.
Why does my browser call fail but curl works?
Almost always CORS. Browsers block cross-origin responses unless the server allows them with Access-Control-Allow-Origin headers.