REST is a set of conventions for exposing data over HTTP. Once you follow them, any client — a browser, a mobile app, a script — can use your API without reading your code.
Learning objectives
- Model data as resources and URLs.
- Choose the right HTTP method and status code.
- Build a CRUD API with Express.
- Validate input and return useful errors.
1. Resources and URLs
A resource is a noun, plural, and stable:
/api/memos the collection
/api/memos/42 one memo
Verbs belong in the HTTP method, not the URL. GET /api/getMemos is not REST; GET /api/memos is.
2. Methods and status codes
| Method | Purpose | Success |
|---|---|---|
| GET | read | 200 OK |
| POST | create | 201 Created |
| PUT / PATCH | replace / update | 200 OK |
| DELETE | remove | 200 OK or 204 No Content |
Errors: 400 invalid input, 401 not signed in, 403 not allowed, 404 not found, 409 conflict, 500 server bug.
3. A complete CRUD API
const express = require("express");
const app = express();
app.use(express.json());
let memos = [
{ id: 1, title: "First memo", content: "Hello", createdAt: new Date().toISOString() },
];
// list, with optional filter and sort
app.get("/api/memos", (req, res) => {
let result = [...memos];
if (req.query.title) {
const q = String(req.query.title).toLowerCase();
result = result.filter((m) => m.title.toLowerCase().includes(q));
}
result.sort((a, b) =>
req.query.sort === "oldest" ? a.id - b.id : b.id - a.id,
);
res.json({ success: true, count: result.length, data: result });
});
// read one
app.get("/api/memos/:id", (req, res) => {
const memo = memos.find((m) => m.id === Number(req.params.id));
if (!memo) {
return res.status(404).json({ success: false, error: "Memo not found" });
}
res.json({ success: true, data: memo });
});
// create
app.post("/api/memos", (req, res) => {
const { title, content } = req.body;
if (!title || !title.trim()) {
return res.status(400).json({ success: false, error: "title is required" });
}
const memo = {
id: Date.now(),
title: title.trim(),
content: (content ?? "").trim(),
createdAt: new Date().toISOString(),
};
memos.push(memo);
res.status(201).json({ success: true, data: memo });
});
// update
app.put("/api/memos/:id", (req, res) => {
const memo = memos.find((m) => m.id === Number(req.params.id));
if (!memo) {
return res.status(404).json({ success: false, error: "Memo not found" });
}
const { title, content } = req.body;
if (!title || !title.trim()) {
return res.status(400).json({ success: false, error: "title is required" });
}
memo.title = title.trim();
if (content !== undefined) memo.content = content.trim();
res.json({ success: true, data: memo });
});
// delete
app.delete("/api/memos/:id", (req, res) => {
const index = memos.findIndex((m) => m.id === Number(req.params.id));
if (index === -1) {
return res.status(404).json({ success: false, error: "Memo not found" });
}
const [deleted] = memos.splice(index, 1);
res.json({ success: true, data: deleted });
});
app.listen(3000);
4. Rules that keep APIs predictable
- Always return the same envelope:
{ success, data }or{ success, error }. - Validate at the boundary. Never trust
req.body. returnafter sending an error response, otherwise the handler continues and Express throws "headers already sent".- Keep IDs stable.
Date.now()is fine for a demo; a database handles this properly in the next subject.
5. Testing without a frontend
curl http://localhost:3000/api/memos
curl -X POST http://localhost:3000/api/memos \
-H "Content-Type: application/json" \
-d "{\"title\":\"Test\",\"content\":\"Body\"}"
Common mistakes
- Using
GETfor anything that changes data. - Returning
200for a created resource or for an error. - Forgetting
express.json(), then reading an emptyreq.body. - Comparing
req.params.id(a string) with numeric IDs using===.
Practice
Implement the memo API in the assignment. Test every status code with curl before writing any frontend.