Topic 7: CRUD Operations
📖 11 min read · 🎯 Intermediate · 🧭 Prerequisites: reading-post-data, basics-picker-status-bar-async-storage
Why this matters
Here's the thing — almost every app you've ever used is doing the same four things with data: adding it, reading it, changing it, and deleting it. Your Instagram post? That's a Create. Scrolling your feed? That's a Read. Editing your bio? Update. Deleting an old photo? Delete. These four operations — Create, Read, Update, Delete — are the heartbeat of every data-driven application. In this lesson, we're going to build a full CRUD API using Express, then connect it to a live web interface. Once this clicks, you'll see it everywhere.
What You'll Learn
- Define the four CRUD operations and map them to HTTP methods (POST, GET, PUT, DELETE)
- Bootstrap an Express server with JSON and URL-encoded body parsing middleware
- Implement all four CRUD endpoints against an in-memory data store
- Test endpoints manually with Postman
- Serve a static HTML page with JavaScript
fetchcalls that exercise every endpoint
The Analogy
Think of a library's card catalogue — a physical cabinet of index cards, one per book. Adding a new card is Create. Flipping through to find a title is Read. Erasing an author's name and writing a correction is Update. Pulling a card out and throwing it away is Delete. Your Express server is the librarian who guards that cabinet, and HTTP requests are the patrons sliding notes through the service window. The cabinet itself — in this lesson, a plain JavaScript array — is your data store.
Chapter 1: Introduction to CRUD Operations
CRUD is the backbone of any application that manages persistent (or semi-persistent) data. Every social post, e-commerce product, user account, and todo item is created, read, updated, and deleted at some point in its lifecycle.
The four operations map cleanly onto standard HTTP methods:
| Operation | HTTP Method | Typical Status Code |
|---|---|---|
| Create | POST | 201 Created |
| Read (all) | GET | 200 OK |
| Read (one) | GET | 200 OK / 404 Not Found |
| Update | PUT | 200 OK / 404 Not Found |
| Delete | DELETE | 200 OK / 404 Not Found |
- Create — Add new data to the store.
- Read — Retrieve one or many existing records.
- Update — Modify an existing record in place.
- Delete — Remove a record permanently.
Chapter 2: Setting Up the Project
Step 1: Create a New Node.js Project
mkdir crud-demo
cd crud-demo
npm init -y
npm install express
This scaffolds a package.json and installs Express as the only runtime dependency.
Step 2: Create the Server File
Create server.js at the project root. The file wires up all four CRUD routes against an in-memory books array that seeds two classics on startup.
server.js:
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 decisions worth noting:
express.json()parsesapplication/jsonrequest bodies intoreq.body.express.urlencoded({ extended: true })handles HTML form submissions.- The
idassigned on creation isbooks.length + 1— simple for demos, but not collision-safe after deletions in production. A UUID library handles that in real apps. books.splice(bookIndex, 1)mutates the array and returns the removed element, which the DELETE handler sends back as confirmation.
Step 3: Run the Server
node server.js
The API is now live at http://localhost:3000. Tools like Postman or curl can exercise every endpoint, and a browser hitting GET /books will return the seed JSON.
Chapter 3: Testing CRUD Endpoints with Postman
Postman (or any HTTP client — Insomnia, curl, HTTPie) lets the class fire targeted requests at each route without building a UI first.
Step 1: Test CREATE — POST /books
- URL:
http://localhost:3000/books - Method:
POST - Body (JSON):
{
"title": "To Kill a Mockingbird",
"author": "Harper Lee"
}
Send the request. The server responds with 201 Created and the newly inserted book object, including its assigned id.
Step 2: Test READ Operations
GET all books
- URL:
http://localhost:3000/books - Method:
GET
Returns the full books array as a JSON array.
GET a single book by ID
- URL:
http://localhost:3000/books/1 - Method:
GET
Returns the book whose id equals 1, or a 404 if none exists.
Step 3: Test UPDATE — PUT /books/:id
- URL:
http://localhost:3000/books/1 - Method:
PUT - Body (JSON):
{
"title": "Animal Farm",
"author": "George Orwell"
}
The server finds the book with id: 1, overwrites both title and author in place, and returns the updated object.
Step 4: Test DELETE — DELETE /books/:id
- URL:
http://localhost:3000/books/1 - Method:
DELETE
The server splices the book out of the array and responds with the removed record. A subsequent GET /books/1 returns 404.
Chapter 4: Creating a Web Interface for CRUD Operations
The class doesn't want to live inside Postman forever. They add a browser UI that calls the same API using the fetch API.
Step 1: Create HTML Forms
Create the directory public/ inside the project root, then add public/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 on page load
fetchBooks();
</script>
</body>
</html>
Every form prevents the default browser submission with e.preventDefault() and instead fires a fetch request to the appropriate endpoint. After each mutation (create, update, delete) the page calls fetchBooks() to refresh the displayed list.
Step 2: Serve the HTML File
Update server.js to add static file serving. Everything else stays identical:
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 from the public/ directory
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}`);
});
express.static intercepts requests for files inside public/ before any route handler runs. A request to http://localhost:3000/ returns public/index.html automatically.
Step 3: Run the Server and Test the Interface
node server.js
Navigate to http://localhost:3000 in a browser. The page loads with the two seeded books already listed. Use the forms to create, update, and delete books — the list refreshes after every action.
Request/Response Flow
sequenceDiagram
participant Browser
participant Express
participant BooksArray
Browser->>Express: POST /books {title, author}
Express->>BooksArray: push(newBook)
BooksArray-->>Express: updated array
Express-->>Browser: 201 {id, title, author}
Browser->>Express: GET /books
Express->>BooksArray: books
BooksArray-->>Express: [{...}, {...}]
Express-->>Browser: 200 [{...}, {...}]
Browser->>Express: PUT /books/1 {title, author}
Express->>BooksArray: find & mutate
BooksArray-->>Express: updatedBook
Express-->>Browser: 200 updatedBook
Browser->>Express: DELETE /books/1
Express->>BooksArray: splice(index, 1)
BooksArray-->>Express: [deletedBook]
Express-->>Browser: 200 [deletedBook]
🧪 Try It Yourself
Task: Extend the book registry with a PATCH endpoint that updates only the title, leaving the author unchanged.
Success criterion: Sending the request below should update the title of book 1 to "Nineteen Eighty-Four" without touching author. A subsequent GET /books/1 should return { id: 1, title: "Nineteen Eighty-Four", author: "George Orwell" }.
Starter snippet — add this to server.js alongside the other routes:
// PATCH: Update only the title of a book by ID
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');
if (req.body.title !== undefined) book.title = req.body.title;
res.json(book);
});
Test it in Postman:
- URL:
http://localhost:3000/books/1 - Method:
PATCH - Body (JSON):
{ "title": "Nineteen Eighty-Four" }
🔍 Checkpoint Quiz
Q1. Which HTTP method and status code should a well-behaved REST API return after successfully creating a new resource?
A) GET with 200 OK
B) POST with 201 Created
C) PUT with 200 OK
D) POST with 200 OK
Q2. Given this snippet from server.js, what does the server respond with when a client sends DELETE /books/99 and no book with id: 99 exists in the array?
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) 200 OK with an empty array
B) 204 No Content
C) 404 with the plain text "Book not found"
D) A JavaScript runtime error
Q3. What is the role of app.use(express.static(path.join(__dirname, 'public'))) in the updated server.js?
A) It compresses all outgoing responses for performance.
B) It serves files from the public/ directory automatically, so index.html loads at /.
C) It prevents Express from processing any further routes after a static file is found.
D) It adds CORS headers to every response.
Q4. The in-memory books array assigns id as books.length + 1. A developer creates three books (ids 1, 2, 3), deletes book 2, then creates a fourth book. What id does the fourth book receive, and what problem does this reveal?
A1. B) POST with 201 Created — POST is the standard method for resource creation, and 201 explicitly signals that a new resource was created, as opposed to 200 which means the request succeeded without necessarily creating anything new.
A2. C) 404 with the plain text "Book not found" — findIndex returns -1 when no match exists, which triggers the early return res.status(404).send('Book not found') before splice is ever called.
A3. B) It serves files from the public/ directory automatically — express.static maps incoming GET requests for file paths to the public/ folder, so a browser navigating to http://localhost:3000/ receives public/index.html without any explicit route handler.
A4. The fourth book receives id: 3 (because books.length is now 2 after the deletion, so 2 + 1 = 3), creating a duplicate ID collision with any system that still references the deleted book 2 by id 3. In production, use a UUID generator (e.g., the uuid npm package) or a database-managed auto-increment to avoid this.
🪞 Recap
- CRUD maps directly to HTTP:
POSTcreates,GETreads,PUTupdates,DELETEremoves. - Express middleware
express.json()andexpress.urlencoded()must be registered before route handlers to makereq.bodyavailable. - Route parameters like
:idare accessible viareq.params.idand must be cast withparseIntwhen compared against numeric array values. express.staticserves an entire directory of assets (HTML, CSS, JS) with a single line, removing the need for individual file-serving routes.- An in-memory array is sufficient for learning, but
idassignment viaarray.length + 1is collision-prone after deletions — swap it for UUIDs in any real project.
📚 Further Reading
- Express.js Routing docs — the source of truth on route parameters, middleware ordering, and HTTP method handlers
- MDN — Using Fetch — comprehensive guide to the browser
fetchAPI used in the web interface - REST API Design Best Practices — deeper dive into status codes, idempotency, and resource naming conventions
- ⬅️ Previous: Basics: Picker, Status Bar & AsyncStorage
- ➡️ Next: Multi-Processing in Node.js