Tutorial · Beginner
REST API Tutorial: from zero to your first own API
REST is the de facto standard for how programs talk to each other over HTTP. In this tutorial you'll learn the fundamentals – HTTP methods, status codes, CRUD – and at the end you'll build a working API with Node.js and Express.
What is a REST API?
REST (Representational State Transfer) is an architectural style for web interfaces. Instead of its own commands, a REST API uses the web's existing mechanisms: URLs address resources (e.g. books, users, orders), and the HTTP method defines what should happen to the resource.
Two properties are central:
- Resource-oriented: A URL like
/api/v1/books/42denotes exactly one object. You use plural nouns – not verbs like/getBuch. - Stateless: Every request contains all the information the server needs. The server remembers nothing between two requests – which makes APIs easy to scale.
HTTP basics: methods, headers, body
An HTTP request consists of a method (the verb), a URL,
optional headers (metadata such as Content-Type or Authorization)
and – for write operations – a body (usually JSON).
| Method | Purpose | Body? | Idempotent? |
|---|---|---|---|
GET | Read resource(s) | no | yes |
POST | Create a new resource | yes | no |
PUT | Fully replace a resource | yes | yes |
PATCH | Partially update a resource | yes | no |
DELETE | Delete a resource | no | yes |
Idempotent means: sending the same request multiple times has the same effect as sending it
once. Sending a PUT with the same content twice changes nothing after the first time –
whereas POST would create two objects.
Understanding status codes
The server always responds with a three-digit status code. The first digit reveals the category:
| Range | Meaning | Common codes |
|---|---|---|
2xx | Success | 200 OK · 201 Created · 204 No Content |
3xx | Redirection | 301 Moved Permanently · 304 Not Modified |
4xx | Client error | 400 Bad Request · 401 Unauthorized · 403 Forbidden · 404 Not Found · 429 Too Many Requests |
5xx | Server error | 500 Internal Server Error · 503 Service Unavailable |
4xx = "you made a mistake" (bad input, missing authentication).
5xx = "the server made a mistake". Use codes deliberately – a
200 with an error message in the body is bad style.
CRUD: the four basic operations
Almost every API maps CRUD – Create, Read, Update, Delete. These four operations map directly onto HTTP methods:
| Operation | HTTP | Example route | Success code |
|---|---|---|---|
| Create | POST | /api/v1/books | 201 |
| Read (list) | GET | /api/v1/books | 200 |
| Read (single) | GET | /api/v1/books/42 | 200 |
| Update | PUT | /api/v1/books/42 | 200 |
| Delete | DELETE | /api/v1/books/42 | 204 |
Hands-on: your first API with Express 5
Now let's build it ourselves. We use Express, the most widely used web framework for Node.js. Express 5 has been the stable version since October 2024 and requires Node.js 18 or newer.
Step 1: Create the project
mkdir books-api && cd books-api
npm init -y
npm install express
node --version # should print v18 or newer
Step 2: Write the server
Create a file server.js with the following content. For simplicity we store the books
in memory (in reality a database would go here).
const express = require("express");
const app = express();
// Parse JSON bodies automatically
app.use(express.json());
// In-memory "database"
let books = [
{ id: 1, title: "Clean Code", author: "Robert C. Martin" },
{ id: 2, title: "The Pragmatic Programmer", author: "Hunt & Thomas" }
];
let nextId = 3;
// READ – all books
app.get("/api/v1/books", (req, res) => {
res.json(books);
});
// READ – a single book (with 404 handling)
app.get("/api/v1/books/:id", (req, res) => {
const book = books.find(b => b.id === Number(req.params.id));
if (!book) {
return res.status(404).json({ error: "Book not found" });
}
res.json(book);
});
// CREATE – add a new book
app.post("/api/v1/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.push(book);
res
.status(201)
.location(`/api/v1/books/${book.id}`)
.json(book);
});
// UPDATE – fully replace a book
app.put("/api/v1/books/:id", (req, res) => {
const book = books.find(b => b.id === Number(req.params.id));
if (!book) {
return res.status(404).json({ error: "Book not found" });
}
const { title, author } = req.body;
book.title = title;
book.author = author;
res.json(book);
});
// DELETE – delete a book
app.delete("/api/v1/books/:id", (req, res) => {
const index = books.findIndex(b => b.id === Number(req.params.id));
if (index === -1) {
return res.status(404).json({ error: "Book not found" });
}
books.splice(index, 1);
res.status(204).end(); // 204 = success without a body
});
app.listen(3000, () => {
console.log("API läuft auf http://localhost:3000");
});
Step 3: Start the server
node server.js
# → API läuft auf http://localhost:3000
Testing the API with curl
Open a second terminal and hit the endpoints:
# Fetch all books (GET)
curl http://localhost:3000/api/v1/books
# Create a new book (POST) – response: 201 Created
curl -X POST http://localhost:3000/api/v1/books \
-H "Content-Type: application/json" \
-d '{"title":"Refactoring","author":"Martin Fowler"}'
# Update a book (PUT)
curl -X PUT http://localhost:3000/api/v1/books/1 \
-H "Content-Type: application/json" \
-d '{"title":"Clean Code (2. Aufl.)","author":"Robert C. Martin"}'
# Delete a book (DELETE) – response: 204, use -i to see the status
curl -X DELETE http://localhost:3000/api/v1/books/1 -i
For graphical testing, Postman, Insomnia or the open-source Bruno are handy – there you can save requests and organise them into collections.
Best practices
- Plural nouns for resources (
/books, not/buchListe). - Version your API via the path (
/api/v1/…). That way you can introduce breaking changes later without breaking existing clients. - Use appropriate status codes – especially
201on creation,204on deletion and400/404for errors. - Validate inputs. Never trust the client. Check required fields and types
(libraries like
zodorexpress-validatorhelp). - A consistent error format, e.g. always
{ "error": "…" }. - Paginate large lists (
?seite=2&limit=20) instead of sending thousands of objects at once. - Enforce HTTPS and secure your write endpoints.
One detail about Express 5: if an async handler throws an error or a promise is
rejected, that error is automatically forwarded to the error middleware –
the tedious try/catch around every handler is no longer needed.
// Async handler without try/catch (Express 5 forwards the error)
app.get("/api/v1/status", async (req, res) => {
const daten = await ladeDaten(); // may throw an error
res.json(daten);
});
// Central error middleware – define it right at the end
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: "Interner Serverfehler" });
});
A REST API addresses resources via URLs, uses HTTP methods as verbs and responds with meaningful status codes. With around 40 lines of Express you have a complete CRUD API. The next logical step: securing the write endpoints.
Did this tutorial help you?
This content is free. I'd really appreciate a coffee. ☕