Topic 7: CRUD Operations
📖 11 min read · 🎯 advanced · 🧭 Prerequisites: file-system, reading-post-data
Why this matters
Here's the thing — every app you've ever used, whether it's booking a train ticket, posting a photo, or updating your profile, is doing exactly four things with data: creating it, reading it, updating it, and deleting it. That's it. Those four operations — CRUD — are the heartbeat of every database-driven application on the planet. In this lesson, we're going to wire all four to a real Express server and build a simple web interface you can actually click through. Once this clicks, you'll see it everywhere.
What You'll Learn
- What the four CRUD operations map to in HTTP terms
- How to scaffold an Express server that handles all four operations against an in-memory data store
- How to test each endpoint manually with Postman
- How to build and serve an HTML/JavaScript front-end that calls those endpoints from the browser
The Analogy
Think of a library catalogue card system. When the librarian adds a new acquisition, she writes a fresh index card — that's Create. When a patron asks "what books do you have on dystopias?", she flips through the drawers — that's Read. When a title gets a new edition, she crosses out the old details and writes in the new ones — that's Update. And when a book is permanently deaccessioned, she pulls the card out and shreds it — that's Delete. Every database-backed web application you will ever build is just this card catalogue at internet scale.
Chapter 1: Introduction to CRUD Operations
CRUD is the shorthand for the four operations that cover virtually every interaction an application has with persistent data.
| Operation | HTTP Method | What it does |
|---|---|---|
| Create | POST | Add new data |
| Read | GET | Retrieve existing data |
| Update | PUT / PATCH | Modify existing data |
| Delete | DELETE | Remove existing data |
These four operations translate directly onto HTTP verbs, which is why REST APIs feel so natural once you understand CRUD. Express gives us a method-per-verb router (app.post, app.get, app.put, app.delete) so the mapping is almost one-to-one.
flowchart LR
Client -->|POST /books| C[Create]
Client -->|GET /books| R[Read]
Client -->|PUT /books/:id| U[Update]
Client -->|DELETE /books/:id| D[Delete]
C & R & U & D --> Store[(In-Memory Store)]
Chapter 2: Setting Up the Project
Step 1: Create a new Node.js project
Open a terminal and run:
mkdir crud-demo
cd crud-demo
npm init -y
npm install express
This creates package.json, installs Express, and puts it in node_modules.
Step 2: Create the server file
Create server.js at the project root. The in-memory books array acts as the data store for this lesson — no database required.
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware to parse JSON and URL-encoded data
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
let books = [
{ id: 1, title: '1984', author: 'George Orwell' },
{ id: 2, title: 'Brave New World', author: 'Aldous Huxley' },
];
// CREATE: Add a new book
app.post('/books', (req, res) => {
const newBook = {
id: books.length + 1,
title: req.body.title,
author: req.body.author,
};
books.push(newBook);
res.status(201).json(newBook);
});
// READ: Get all books
app.get('/books', (req, res) => {
res.json(books);
});
// READ: Get a book by ID
app.get('/books/:id', (req, res) => {
const book = books.find(b => b.id === parseInt(req.params.id));
if (!book) return res.status(404).send('Book not found');
res.json(book);
});
// UPDATE: Update a book by ID
app.put('/books/:id', (req, res) => {
const book = books.find(b => b.id === parseInt(req.params.id));
if (!book) return res.status(404).send('Book not found');
book.title = req.body.title;
book.author = req.body.author;
res.json(book);
});
// DELETE: Delete a book by ID
app.delete('/books/:id', (req, res) => {
const bookIndex = books.findIndex(b => b.id === parseInt(req.params.id));
if (bookIndex === -1) return res.status(404).send('Book not found');
const deletedBook = books.splice(bookIndex, 1);
res.json(deletedBook);
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Key points to note:
express.json()parses incoming requests with aContent-Type: application/jsonheader.express.urlencoded({ extended: true })handles form-submitted data.res.status(201)on the POST signals "resource created" — more precise than a plain200.books.splice(bookIndex, 1)removes the element at the found index and returns it as an array, which is then sent back to the caller.parseInt(req.params.id)is required because route params arrive as strings; the===comparison against numeric IDs in the array would otherwise always fail.
Step 3: Run the server
node server.js
The API is now running at http://localhost:3000. You can use Postman, curl, or a web form to interact with it.
Chapter 3: Testing CRUD Endpoints with Postman
Postman is a GUI HTTP client that makes it easy to craft requests with custom methods, headers, and bodies.
Step 1: Test the CREATE operation
- Method:
POST - URL:
http://localhost:3000/books - Body tab → raw → JSON:
{
"title": "To Kill a Mockingbird",
"author": "Harper Lee"
}
Send the request. You should receive a 201 Created response with the new book object, including its auto-assigned id.
Step 2: Test the READ operations
Get all books:
- Method:
GET - URL:
http://localhost:3000/books - Send — you'll receive the full array including the book you just created.
Get one book by ID:
- Method:
GET - URL:
http://localhost:3000/books/1 - Send — you'll receive only the book with
id: 1.
Step 3: Test the UPDATE operation
- Method:
PUT - URL:
http://localhost:3000/books/1 - Body tab → raw → JSON:
{
"title": "Animal Farm",
"author": "George Orwell"
}
Send — the server will overwrite the title and author fields of book 1 and return the updated object.
Step 4: Test the DELETE operation
- Method:
DELETE - URL:
http://localhost:3000/books/1 - Send — the server splices that book out of the array and returns it.
If you send a GET to /books again, you'll confirm the deleted entry is gone.
Chapter 4: Creating a Web Interface for CRUD Operations
A REST API is only as useful as the clients that consume it. The class built a simple browser-based front-end so non-technical users could manage the book list without touching Postman.
Step 1: Create the HTML interface
Create the directory public/ and inside it create index.html:
<!DOCTYPE html>
<html>
<head>
<title>Book Management</title>
</head>
<body>
<h1>Create Book</h1>
<form id="createForm">
<label for="createTitle">Title:</label>
<input type="text" id="createTitle" name="title" required>
<label for="createAuthor">Author:</label>
<input type="text" id="createAuthor" name="author" required>
<button type="submit">Create</button>
</form>
<h1>Update Book</h1>
<form id="updateForm">
<label for="updateId">ID:</label>
<input type="number" id="updateId" name="id" required>
<label for="updateTitle">Title:</label>
<input type="text" id="updateTitle" name="title" required>
<label for="updateAuthor">Author:</label>
<input type="text" id="updateAuthor" name="author" required>
<button type="submit">Update</button>
</form>
<h1>Delete Book</h1>
<form id="deleteForm">
<label for="deleteId">ID:</label>
<input type="number" id="deleteId" name="id" required>
<button type="submit">Delete</button>
</form>
<h1>Books</h1>
<button onclick="fetchBooks()">Fetch Books</button>
<ul id="booksList"></ul>
<script>
document.getElementById('createForm').addEventListener('submit', function(e) {
e.preventDefault();
const title = document.getElementById('createTitle').value;
const author = document.getElementById('createAuthor').value;
fetch('/books', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ title, author })
})
.then(response => response.json())
.then(data => {
console.log('Book created:', data);
fetchBooks();
});
});
document.getElementById('updateForm').addEventListener('submit', function(e) {
e.preventDefault();
const id = document.getElementById('updateId').value;
const title = document.getElementById('updateTitle').value;
const author = document.getElementById('updateAuthor').value;
fetch(`/books/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ title, author })
})
.then(response => response.json())
.then(data => {
console.log('Book updated:', data);
fetchBooks();
});
});
document.getElementById('deleteForm').addEventListener('submit', function(e) {
e.preventDefault();
const id = document.getElementById('deleteId').value;
fetch(`/books/${id}`, {
method: 'DELETE'
})
.then(response => {
console.log('Book deleted:', response);
fetchBooks();
});
});
function fetchBooks() {
fetch('/books')
.then(response => response.json())
.then(data => {
const booksList = document.getElementById('booksList');
booksList.innerHTML = '';
data.forEach(book => {
const listItem = document.createElement('li');
listItem.textContent = `${book.id}. ${book.title} by ${book.author}`;
booksList.appendChild(listItem);
});
});
}
// Initial fetch
fetchBooks();
</script>
</body>
</html>
Step 2: Serve the HTML file from Express
Update server.js to include path and the express.static middleware so the server can deliver public/index.html:
const express = require('express');
const path = require('path');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware to parse JSON and URL-encoded data
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Serve static files
app.use(express.static(path.join(__dirname, 'public')));
let books = [
{ id: 1, title: '1984', author: 'George Orwell' },
{ id: 2, title: 'Brave New World', author: 'Aldous Huxley' },
];
// CREATE: Add a new book
app.post('/books', (req, res) => {
const newBook = {
id: books.length + 1,
title: req.body.title,
author: req.body.author,
};
books.push(newBook);
res.status(201).json(newBook);
});
// READ: Get all books
app.get('/books', (req, res) => {
res.json(books);
});
// READ: Get a book by ID
app.get('/books/:id', (req, res) => {
const book = books.find(b => b.id === parseInt(req.params.id));
if (!book) return res.status(404).send('Book not found');
res.json(book);
});
// UPDATE: Update a book by ID
app.put('/books/:id', (req, res) => {
const book = books.find(b => b.id === parseInt(req.params.id));
if (!book) return res.status(404).send('Book not found');
book.title = req.body.title;
book.author = req.body.author;
res.json(book);
});
// DELETE: Delete a book by ID
app.delete('/books/:id', (req, res) => {
const bookIndex = books.findIndex(b => b.id === parseInt(req.params.id));
if (bookIndex === -1) return res.status(404).send('Book not found');
const deletedBook = books.splice(bookIndex, 1);
res.json(deletedBook);
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
What changed: const path = require('path') and app.use(express.static(path.join(__dirname, 'public'))) were added. express.static maps all requests for static assets to the public/ directory, so GET / automatically serves public/index.html.
Step 3: Run the server and test the interface
node server.js
Open http://localhost:3000 in a browser. The web interface lets you:
- Fill in the Create Book form and click Create to POST a new book.
- Supply an ID, new title, and author in Update Book and click Update to PUT changes.
- Type an ID in Delete Book and click Delete to DELETE that record.
- Click Fetch Books (or let the page load) to GET and render the current list.
Every form submission automatically calls fetchBooks() afterward so the list stays in sync without a manual page reload.
🧪 Try It Yourself
Task: Extend the API to support a PATCH /books/:id endpoint that updates only the fields present in the request body — leaving the other field unchanged.
Success criterion: After sending PATCH /books/1 with body { "title": "Homage to Catalonia" }, a subsequent GET /books/1 should return { id: 1, title: "Homage to Catalonia", author: "George Orwell" } — the author must be unchanged.
Starter snippet:
app.patch('/books/:id', (req, res) => {
const book = books.find(b => b.id === parseInt(req.params.id));
if (!book) return res.status(404).send('Book not found');
// TODO: only overwrite keys that exist in req.body
res.json(book);
});
Hint: Object.assign(book, req.body) will merge only the provided keys.
🔍 Checkpoint Quiz
Q1. Why does the POST handler respond with res.status(201) instead of the default res.status(200)?
A) Because 200 is reserved for GET requests only
B) Because 201 Created semantically signals that a new resource was created, which is more precise
C) Because Express will throw an error if you use 200 on a POST route
D) Because Postman only accepts 201 from POST endpoints
Q2. Given this route handler, what does the server send back when a client requests DELETE /books/99 and no book with ID 99 exists?
app.delete('/books/:id', (req, res) => {
const bookIndex = books.findIndex(b => b.id === parseInt(req.params.id));
if (bookIndex === -1) return res.status(404).send('Book not found');
const deletedBook = books.splice(bookIndex, 1);
res.json(deletedBook);
});
A) An empty JSON array []
B) A 500 Internal Server Error
C) A 404 response with the text Book not found
D) null
Q3. What would happen if you removed the parseInt() call from req.params.id comparisons throughout the server, and why?
A) Nothing — JavaScript coerces string "1" to number 1 automatically in all comparisons
B) The find and findIndex calls would always return undefined / -1 because "1" === 1 is false in strict equality
C) Only the DELETE route would break; GET and PUT use loose equality
D) Express would throw a type error at startup
Q4. You want to display the books list without a manual button click — the page should fetch and render them automatically when it loads. Looking at the existing public/index.html, how does it already achieve this?
A) It uses a <meta http-equiv="refresh"> tag
B) It calls fetchBooks() once at the bottom of the <script> block as an immediate invocation
C) Express pushes data over a WebSocket connection
D) The browser caches the GET response and renders it on load
A1. B — 201 Created is the correct HTTP status for a successful resource-creation response. Using 200 would work but is semantically imprecise; REST conventions encourage matching status codes to the type of action performed.
A2. C — bookIndex is -1, so the early-return guard fires immediately and sends a 404 with the plain-text body Book not found. The splice line is never reached.
A3. B — Route params are always strings. books.find(b => b.id === "1") uses strict equality (===), so the string "1" never equals the number 1, causing every lookup to fail and every request to return a 404.
A4. B — The very last line inside the <script> block is a bare call fetchBooks(); (marked with the comment // Initial fetch). This runs once when the script executes on page load, populating the list immediately without any user interaction.
🪞 Recap
- CRUD maps directly to HTTP verbs:
POST= Create,GET= Read,PUT= Update,DELETE= Delete. - Express provides a method-per-verb router (
app.post,app.get,app.put,app.delete) that makes the mapping nearly one-to-one. parseInt(req.params.id)is essential when comparing URL params against numeric IDs because all route parameters arrive as strings.express.staticserves an entirepublic/directory as static assets with a single middleware line, eliminating the need to write individual GET routes for HTML, CSS, and JS files.- A browser-side
fetchAPI call with the rightmethodandContent-Type: application/jsonheader is all you need to drive a REST API from plain HTML.
📚 Further Reading
- Express.js routing docs — the source of truth on
app.METHOD, route params, and middleware ordering - MDN Fetch API — deep dive into browser-side
fetch, headers, and response handling - HTTP Status Codes reference — understand when to use 200, 201, 404, and 500 in your responses
- ⬅️ Previous: Reading POST Data
- ➡️ Next: Multi-Processing in Node.js