Learnwizy Technologies Logo

Learnwizy Technologies

Class 26: Building a RESTful API with Express (CRUD - Delete)

Today, we'll complete our CRUD API by implementing the "Delete" operation. We'll also learn how to organize our routes using Express's Router, which is crucial for larger applications. Finally, we'll touch upon API versioning and enhanced error handling.


Implementing DELETE Requests

The DELETE method is used to remove a specific resource from the server.

// server.js
const express = require('express');
const app = express();
const port = 3000;

let books = [
  { id: '1', title: 'The Hitchhiker\'s Guide to the Galaxy', author: 'Douglas Adams', year: 1979 },
  { id: '2', title: '1984', author: 'George Orwell', year: 1949 },
  { id: '3', title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960 }
];

app.use(express.json());

// GET all books
app.get('/api/books', (req, res) => {
  res.status(200).json(books);
});

// GET single book by ID
app.get('/api/books/:id', (req, res) => {
  const book = books.find(b => b.id === req.params.id);
  if (book) {
    res.status(200).json(book);
  } else {
    res.status(404).json({ message: `Book with ID ${req.params.id} not found.` });
  }
});

// POST create book
app.post('/api/books', (req, res) => {
  const { title, author, year } = req.body;
  if (!title || !author || !year) {
    return res.status(400).json({ message: 'Title, author, and year are required.' });
  }
  const newId = (books.length > 0 ? Math.max(...books.map(b => parseInt(b.id))) + 1 : 1).toString();
  const newBook = { id: newId, title, author, year: parseInt(year) };
  books.push(newBook);
  res.status(201).json(newBook);
});

// PUT update book
app.put('/api/books/:id', (req, res) => {
  const bookId = req.params.id;
  const { title, author, year } = req.body;
  if (!title || !author || !year) {
    return res.status(400).json({ message: 'Title, author, and year are required for a full update.' });
  }
  const bookIndex = books.findIndex(b => b.id === bookId);
  if (bookIndex !== -1) {
    books[bookIndex] = { id: bookId, title, author, year: parseInt(year) };
    res.status(200).json(books[bookIndex]);
  } else {
    res.status(404).json({ message: `Book with ID ${bookId} not found for update.` });
  }
});

// PATCH update book
app.patch('/api/books/:id', (req, res) => {
  const bookId = req.params.id;
  const updates = req.body;
  const bookIndex = books.findIndex(b => b.id === bookId);
  if (bookIndex !== -1) {
    books[bookIndex] = { ...books[bookIndex], ...updates };
    if (updates.year) {
        books[bookIndex].year = parseInt(updates.year);
    }
    res.status(200).json(books[bookIndex]);
  } else {
    res.status(404).json({ message: `Book with ID ${bookId} not found for partial update.` });
  }
});


// DELETE /api/books/:id - Delete a book
app.delete('/api/books/:id', (req, res) => {
  const bookId = req.params.id;
  const initialLength = books.length;
  books = books.filter(b => b.id !== bookId); // Filter out the book to be deleted

  if (books.length < initialLength) {
    // If length changed, a book was deleted
    res.status(204).send(); // 204 No Content
  } else {
    // No book found with that ID
    res.status(404).json({ message: `Book with ID ${bookId} not found for deletion.` });
  }
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});

Test it with Postman/Insomnia:

  1. First, use a GET request to /api/books to see existing IDs.
  2. Method: DELETE
  3. URL: http://localhost:3000/api/books/1 (replace '1' with an existing ID)
  4. Send the request. You should receive a 204 No Content response. The response body will be empty.
  5. Send a GET request to /api/books again to confirm the book is gone.
  6. Try to delete a non-existent ID (e.g., 99) to see the 404 Not Found response.

Consolidating API Endpoints with express.Router()

As your application grows, putting all routes in a single server.js file becomes unmanageable. express.Router() allows you to create modular, mountable route handlers. This helps organize your code by feature or resource.

Steps:

  1. Create a new file for your resource's routes: e.g., routes/books.js.
  2. Define routes using router instead of app: Inside books.js, create an instance of express.Router() and define all your book-related routes on this router.
  3. Export the router: Make the router available for import in your main application file.
  4. Mount the router in server.js: Use app.use() to "mount" the router at a specific base path (e.g., /api/books).
// routes/books.js
const express = require('express');
const router = express.Router();

// In-memory array (for now, would be database in real app)
let books = [
  { id: '1', title: 'The Hitchhiker\'s Guide to the Galaxy', author: 'Douglas Adams', year: 1979 },
  { id: '2', title: '1984', author: 'George Orwell', year: 1949 },
  { id: '3', title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960 }
];

// GET all books
router.get('/', (req, res) => {
  res.status(200).json(books);
});

// GET single book by ID
router.get('/:id', (req, res) => {
  const book = books.find(b => b.id === req.params.id);
  if (book) {
    res.status(200).json(book);
  } else {
    res.status(404).json({ message: `Book with ID ${req.params.id} not found.` });
  }
});

// POST create book
router.post('/', (req, res) => {
  const { title, author, year } = req.body;
  if (!title || !author || !year) {
    return res.status(400).json({ message: 'Title, author, and year are required.' });
  }
  const newId = (books.length > 0 ? Math.max(...books.map(b => parseInt(b.id))) + 1 : 1).toString();
  const newBook = { id: newId, title, author, year: parseInt(year) };
  books.push(newBook);
  res.status(201).json(newBook);
});

// PUT update book
router.put('/:id', (req, res) => {
  const bookId = req.params.id;
  const { title, author, year } = req.body;
  if (!title || !author || !year) {
    return res.status(400).json({ message: 'Title, author, and year are required for a full update.' });
  }
  const bookIndex = books.findIndex(b => b.id === bookId);
  if (bookIndex !== -1) {
    books[bookIndex] = { id: bookId, title, author, year: parseInt(year) };
    res.status(200).json(books[bookIndex]);
  } else {
    res.status(404).json({ message: `Book with ID ${bookId} not found for update.` });
  }
});

// PATCH update book
router.patch('/:id', (req, res) => {
  const bookId = req.params.id;
  const updates = req.body;
  const bookIndex = books.findIndex(b => b.id === bookId);
  if (bookIndex !== -1) {
    books[bookIndex] = { ...books[bookIndex], ...updates };
    if (updates.year) {
        books[bookIndex].year = parseInt(updates.year);
    }
    res.status(200).json(books[bookIndex]);
  } else {
    res.status(404).json({ message: `Book with ID ${bookId} not found for partial update.` });
  }
});

// DELETE book
router.delete('/:id', (req, res) => {
  const bookId = req.params.id;
  const initialLength = books.length;
  books = books.filter(b => b.id !== bookId);

  if (books.length < initialLength) {
    res.status(204).send();
  } else {
    res.status(404).json({ message: `Book with ID ${bookId} not found for deletion.` });
  }
});

module.exports = router; // Export the router
// server.js (main application file)
const express = require('express');
const app = express();
const port = 3000;

const booksRouter = require('./routes/books'); // Import the books router

app.use(express.json()); // Global middleware for JSON body parsing

// Mount the books router at the /api/books path
// All routes defined in booksRouter will now be prefixed with /api/books
app.use('/api/books', booksRouter);

// Basic root route (optional, for testing server status)
app.get('/', (req, res) => {
    res.send('Welcome to the Book API!');
});

// Start the server
app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});

This modular approach keeps your main server.js clean and makes it easy to add more resources (e.g., /api/users, /api/orders) by creating new router files.


Basic API Design Best Practices


API Versioning

As your API evolves, you might need to make breaking changes that are incompatible with older clients. API versioning allows you to introduce new versions of your API while maintaining backward compatibility for existing clients.

Common versioning strategies:


Enhanced Error Handling in API Responses

While we added basic inline validation, a more robust API will have a centralized error handling mechanism. This involves:

// server.js (main file, add this at the end, after all routes)

// Handle 404 Not Found errors for unhandled routes
app.use((req, res, next) => {
    res.status(404).json({ message: 'Resource not found.' });
});

// Centralized Error Handling Middleware (must be the last middleware)
app.use((err, req, res, next) => {
    console.error(err.stack); // Log the error for debugging purposes

    const statusCode = err.statusCode || 500;
    const message = err.message || 'Something went wrong on the server.';
    const errors = err.errors || []; // For validation errors

    res.status(statusCode).json({
        status: 'error',
        statusCode,
        message,
        errors
    });
});

This error handling pattern makes your API more predictable and easier for clients to consume, as they always know what format to expect for error responses.

You've now built a complete CRUD API with Express.js, including basic error handling and modular routing. In the next class, we'll move beyond in-memory data and connect our API to a real database, starting with relational databases and SQL.