Class 25: Building a RESTful API with Express (CRUD - Create & Update)
Continuing our journey of building a RESTful API with Express.js, today we'll implement the "Create" (POST) and "Update" (PUT/PATCH) operations. These operations involve receiving data from the client in the request body, processing it, and updating our in-memory data store.
Remember, we are still using a JavaScript array to simulate our database.
Implementing POST Requests (Create)
The POST
method is used to create new resources.
When a client sends a POST request to a collection endpoint, it
typically includes the data for the new resource in the request
body.
- Endpoint:
POST /api/books
-
Receiving Data: We'll use the
express.json()
middleware (which we already added inclass24.html
) to parse the JSON data fromreq.body
. - Generating ID: Since we don't have a database automatically generating IDs, we'll manually generate a simple unique ID for our new book.
-
Responding: On successful creation, we
respond with the newly created resource and a
201 Created
status code. - Validation: We'll add some basic server-side validation to ensure required fields are present.
// 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 }
];
// Middleware to parse JSON bodies
app.use(express.json());
// GET all books (from previous class)
app.get('/api/books', (req, res) => {
res.status(200).json(books);
});
// GET single book by ID (from previous class)
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 /api/books - Create a new book
app.post('/api/books', (req, res) => {
const { title, author, year } = req.body; // Destructure data from request body
// Basic validation
if (!title || !author || !year) {
// Send 400 Bad Request if required fields are missing
return res.status(400).json({ message: 'Title, author, and year are required.' });
}
// Generate a simple unique ID (for in-memory data)
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) // Ensure year is a number
};
books.push(newBook); // Add the new book to our array
// Respond with the newly created resource and 201 Created status
res.status(201).json(newBook);
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
Test it with Postman/Insomnia:
- Method:
POST
-
URL:
http://localhost:3000/api/books
-
Headers: Set
Content-Type: application/json
-
Body: Select "raw" and "JSON" and enter:
{ "title": "The Great Gatsby", "author": "F. Scott Fitzgerald", "year": 1925 }
-
Send the request. You should get a
201 Created
response with the new book object, including its generated ID. -
Try sending a request with missing fields (e.g., no
title
) to see the400 Bad Request
response.
Implementing PUT Requests (Full Update)
The PUT
method is used to fully update or replace
an existing resource. The client sends the complete new
representation of the resource in the request body.
-
Endpoint:
PUT /api/books/:id
-
Extracting Data: Get the ID from
req.params.id
and the new data fromreq.body
. -
Logic: Find the book by ID. If found,
replace its entire data with the new data from
req.body
. If not found, you might choose to create it (upsert) or return a 404. For simplicity, we'll return 404 if not found. -
Responding: Respond with the updated
resource and
200 OK
status. Handle404 Not Found
if the book doesn't exist.
// server.js (continued)
// PUT /api/books/:id - Fully update a book
app.put('/api/books/:id', (req, res) => {
const bookId = req.params.id;
const { title, author, year } = req.body;
// Basic validation for full update
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) {
// Update the existing book with new data
books[bookIndex] = {
id: bookId, // Ensure ID remains the same
title,
author,
year: parseInt(year)
};
res.status(200).json(books[bookIndex]); // Respond with the updated book
} else {
// If book not found, return 404
res.status(404).json({ message: `Book with ID ${bookId} not found for update.` });
}
});
// ... app.listen() remains the same
Test it with Postman/Insomnia:
- First, make sure you have a book with ID '1' (or create a new one with POST).
- Method:
PUT
-
URL:
http://localhost:3000/api/books/1
-
Headers: Set
Content-Type: application/json
-
Body: Select "raw" and "JSON" and enter a
complete new representation for the book:
{ "title": "The Updated Hitchhiker's Guide", "author": "Douglas Adams (Revised)", "year": 2020 }
-
Send the request. You should get a
200 OK
response with the fully updated book. -
Try to update a non-existent ID (e.g.,
99
) to see the404 Not Found
response.
Implementing PATCH Requests (Partial Update)
The PATCH
method is used to apply partial
modifications to a resource. The client sends only the fields
they want to update in the request body.
-
Endpoint:
PATCH /api/books/:id
-
Logic: Find the book by ID. If found, merge
the fields from
req.body
into the existing book object. This means only the provided fields are updated, while others remain unchanged. -
Responding: Respond with the updated
resource and
200 OK
status. Handle404 Not Found
if the book doesn't exist.
// server.js (continued)
// PATCH /api/books/:id - Partially update a book
app.patch('/api/books/:id', (req, res) => {
const bookId = req.params.id;
const updates = req.body; // Get the partial updates from the request body
const bookIndex = books.findIndex(b => b.id === bookId);
if (bookIndex !== -1) {
// Merge updates into the existing book object
// Object.assign creates a new object, preventing direct mutation of original
books[bookIndex] = { ...books[bookIndex], ...updates };
// Ensure year is parsed if it's part of the update
if (updates.year) {
books[bookIndex].year = parseInt(updates.year);
}
res.status(200).json(books[bookIndex]); // Respond with the partially updated book
} else {
res.status(404).json({ message: `Book with ID ${bookId} not found for partial update.` });
}
});
// ... app.listen() remains the same
Difference between PUT and PATCH semantics:
-
PUT
: Replaces the entire resource. If you send a PUT request with only one field, all other fields of the resource will be removed or set to default values. It's like replacing a whole document. -
PATCH
: Applies a partial modification to a resource. You send only the fields you want to change, and the server merges those changes with the existing resource. It's like editing specific parts of a document.
Test it with Postman/Insomnia:
- First, make sure you have a book with ID '1'.
- Method:
PATCH
-
URL:
http://localhost:3000/api/books/1
-
Headers: Set
Content-Type: application/json
-
Body: Select "raw" and "JSON" and enter:
{ "title": "The Ultimate Guide", "year": 2025 }
-
Send the request. You should get a
200 OK
response. Notice that only thetitle
andyear
are updated, whileauthor
remains unchanged.
Validation and Error Handling (Intermediate)
We've already implemented basic validation for missing required fields. It's crucial to provide meaningful error messages to the client when something goes wrong.
-
Always send appropriate HTTP status codes (e.g.,
400 Bad Request
for invalid input,404 Not Found
for non-existent resources). - Include a clear, concise message in the JSON response body explaining the error.
// Example of improved validation and error response for POST
app.post('/api/books', (req, res) => {
const { title, author, year } = req.body;
const errors = [];
if (!title) errors.push('Title is required.');
if (!author) errors.push('Author is required.');
if (!year) errors.push('Year is required.');
if (year && isNaN(parseInt(year))) errors.push('Year must be a number.');
if (errors.length > 0) {
return res.status(400).json({ message: 'Validation failed', errors });
}
// ... rest of the creation logic ...
});
This approach provides more specific feedback to the client, making it easier for them to correct their requests.
You've now successfully implemented the Create and Update operations for your RESTful API. In the next class, we'll complete the CRUD cycle by adding the Delete operation and explore how to organize our routes using Express Router.