Topic 32 of 56 · Full Stack Advanced

Topic 7 : CRUD operations

Lesson TL;DRTopic 7: CRUD Operations 📖 11 min read · 🎯 Intermediate · 🧭 Prerequisites: readingpostdata, basicspickerstatusbarasyncstorage Why this matters Here's the thing — almost every app you've ever used i...
11 min read·intermediate·nodejs · express · crud · rest-api

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 fetch calls 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:

OperationHTTP MethodTypical Status Code
CreatePOST201 Created
Read (all)GET200 OK
Read (one)GET200 OK / 404 Not Found
UpdatePUT200 OK / 404 Not Found
DeleteDELETE200 OK / 404 Not Found
  1. Create — Add new data to the store.
  2. Read — Retrieve one or many existing records.
  3. Update — Modify an existing record in place.
  4. 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() parses application/json request bodies into req.body.
  • express.urlencoded({ extended: true }) handles HTML form submissions.
  • The id assigned on creation is books.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 CreatedPOST 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: POST creates, GET reads, PUT updates, DELETE removes.
  • Express middleware express.json() and express.urlencoded() must be registered before route handlers to make req.body available.
  • Route parameters like :id are accessible via req.params.id and must be cast with parseInt when compared against numeric array values.
  • express.static serves 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 id assignment via array.length + 1 is collision-prone after deletions — swap it for UUIDs in any real project.

📚 Further Reading

Like this topic? It’s one of 56 in Full Stack Advanced.

Block your seat for ₹2,500 and join the next cohort.