W04 Learning Activity: Updating and Deleting through APIs
Overview
- Purpose: Understand how using Web Services can helpupdate and delete items in a list without refreshing the entire page.
- Task: Carefully read and understand the concepts in this learning activity. Several people on your team will be responsible for adding this type of functionality to the project.
Your team project includes EJS pages and client-side JavaScript that calls APIs. This makes it possible to update a small section of a page after a user action instead of asking the browser to load the entire page again. The navigation, page layout, and other work the user has already done remain in place.
This learning activity uses an ecommerce product list to demonstrate the pattern. The page loads products, displays Edit and Delete buttons, replaces one card with an edit form, sends a PUT or DELETE request, and refreshes the list in the DOM without a browser-page refresh.
Understand Now. Implement Later.
Read and understand the concepts and code in this learning activity, but you do not need to run each snippet yet. Depending on the features you choose, some members of your team will implement this type of update and delete functionality in the project.
Preparation Material
Updating and deleting through Web Services
When a browser needs to update or delete an item, it can call the web service to make the actual change. The API validates values, checks authentication and authorization, writes to MongoDB, and returns a response. Client-side JavaScript owns the user experience: it displays the list, collects form values, sends requests, and changes the visible list after a successful response.
Do not assume a local edit succeeded just because a user selected Save. The server may reject a request because the product is missing, the values are invalid, or the user lacks permission. The browser should wait for the API response before changing the displayed data.
Plan the interaction
- Load products with
GET /api/products. - Render each product with Edit and Delete buttons.
- When the user clicks the Edit button, replace only the selected card with an edit form.
- Send
PUT /api/products/:idon Save orDELETE /api/products/:idon Delete. - Use the successful response to update the list; do not call
window.location.reload().
Use templates instead of HTML strings
The HTML <template> element holds reusable markup that is not displayed until JavaScript clones it. This keeps structure in HTML, lets code set API values with textContent, and avoids treating API text as HTML through a large innerHTML string.
Create the list and two templates
The display template is used for normal product cards. The edit template is used only while one product is being changed. The buttons use data-action, and each rendered card receives the API product id in data-product-id.
<p id="product-message"></p>
<h2>Products</h2>
<div id="product-list"></div>
<template id="product-card-template">
<div class="product-card" data-product-id="">
<h3 class="product-name"></h3>
<p class="product-category"></p>
<p class="product-price"></p>
<button type="button" data-action="edit">Edit</button>
<button type="button" data-action="delete">Delete</button>
</div>
</template>
<template id="product-edit-template">
<form class="product-edit-form" data-product-id="">
<label>Name <input name="name" required></label>
<label>Category <input name="category" required></label>
<label>Price <input name="price" type="number" min="0" step="0.01" required></label>
<button type="submit">Save</button>
<button type="button" data-action="cancel">Cancel</button>
</form>
</template>
The message area displays success and error messages. The templates are not visible until cloned, so the product list begins empty rather than displaying duplicated example markup.
Load and hydrate the display list
Keep the loaded products in a Map. This lets a button handler find the complete product object without reading values back out of the page. A DocumentFragment prepares every card efficiently, then replaceChildren() changes only the list section.
const productList = document.querySelector('#product-list');
const message = document.querySelector('#product-message');
const cardTemplate = document.querySelector('#product-card-template');
const editTemplate = document.querySelector('#product-edit-template');
let productsById = new Map();
const renderProducts = () => {
const fragment = document.createDocumentFragment();
for (const product of productsById.values()) {
const card = cardTemplate.content.cloneNode(true);
const productCard = card.querySelector('[data-product-id]');
productCard.dataset.productId = product._id;
card.querySelector('.product-name').textContent = product.name;
card.querySelector('.product-category').textContent = product.category;
card.querySelector('.product-price').textContent = `$${Number(product.price).toFixed(2)}`;
fragment.append(card);
}
productList.replaceChildren(fragment);
};
async function loadProducts() {
const response = await fetch('/api/products');
if (!response.ok) throw new Error('Unable to load products.');
const products = await response.json();
productsById = new Map(products.map((product) => [product._id, product]));
renderProducts();
}
loadProducts().catch((error) => { message.textContent = error.message; });
Notice that when setting the API values this code uses textContent. Because of this, a product name that contains HTML-looking characters remains text. The list is re-rendered from the JavaScript map, but the page itself is never reloaded.
Handle buttons with event delegation
Rendered cards are replaced frequently. Instead of adding a listener to every new button, add one listener to the stable list container. Click events bubble up to that container, and closest() finds the selected button and its product card.
productList.addEventListener('click', async (event) => {
const button = event.target.closest('button[data-action]');
if (!button) return;
const card = button.closest('[data-product-id]');
const product = productsById.get(card.dataset.productId);
if (button.dataset.action === 'edit') showEditor(card, product);
if (button.dataset.action === 'cancel') renderProducts();
if (button.dataset.action === 'delete') await deleteProduct(product);
});
Cancel needs no API call because no server data changed. Delete is asynchronous because the browser must wait for the server. In a production interface, you could also disable controls while a request is in progress to prevent duplicate submissions.
Turn one card into an edit form
When a user clicks the edit button, your code should clone the edit template, copy the current product values into its inputs, and replace only the selected card. The form has its own submit listener because it was created dynamically.
function showEditor(card, product) {
const editor = editTemplate.content.cloneNode(true);
const form = editor.querySelector('form');
form.dataset.productId = product._id;
form.elements.name.value = product.name;
form.elements.category.value = product.category;
form.elements.price.value = product.price;
form.addEventListener('submit', saveProduct);
card.replaceWith(editor);
form.elements.name.focus();
}
The id is stored on the form so the request can identify the resource, but the API must still authorize that request. A browser user can change DOM attributes, so browser values are never secure and must be validated on the server.
Save with PUT and refresh the list section
A normal form submission reloads the browser page. preventDefault() stops that default behavior. The function converts form data to JSON, sends it to the API, and updates the local map only after the API returns the updated product.
async function saveProduct(event) {
event.preventDefault();
const form = event.currentTarget;
const body = Object.fromEntries(new FormData(form));
body.price = Number(body.price);
const response = await fetch(
`/api/products/${encodeURIComponent(form.dataset.productId)}`,
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
}
);
if (!response.ok) {
message.textContent = 'The product could not be updated.';
return;
}
const updated = await response.json();
productsById.set(updated._id, updated);
renderProducts();
message.textContent = `${updated.name} was updated.`;
}
Using the API response, rather than the submitted form values, keeps the UI aligned with server validation and formatting. If validation fails, keep or restore the form and show a useful message instead of displaying an unconfirmed change.
Delete with DELETE
A confirmation dialog helps prevent accidental deletion, but it is not authorization. The server must still check the signed-in user's role or ownership before it removes a product.
async function deleteProduct(product) {
if (!window.confirm(`Delete ${product.name}?`)) return;
const response = await fetch(
`/api/products/${encodeURIComponent(product._id)}`,
{ method: 'DELETE' }
);
if (!response.ok) {
message.textContent = 'The product could not be deleted.';
return;
}
productsById.delete(product._id);
renderProducts();
message.textContent = `${product.name} was deleted.`;
}
After a successful response, remove the item from the map and render again. The browser updates the list, but the API remains responsible for the actual delete operation.
Protect the API on the server
Client-side code is not security. Protect update and delete routes with middleware, validate request data, and ask Mongoose to run validators on updates. Return JSON errors so the browser can respond appropriately.
router.put('/products/:id', requireApiRole('admin'), updateProduct);
router.delete('/products/:id', requireApiRole('admin'), deleteProduct);
export async function updateProduct(req, res) {
try {
const allowedUpdates = {};
if (req.body.name !== undefined) allowedUpdates.name = req.body.name;
if (req.body.price !== undefined) allowedUpdates.price = req.body.price;
if (req.body.category !== undefined) allowedUpdates.category = req.body.category;
if (req.body.isActive !== undefined) allowedUpdates.isActive = req.body.isActive;
const product = await Product.findByIdAndUpdate(req.params.id, allowedUpdates, { new: true, runValidators: true });
if (!product) return res.status(404).json({ message: 'Product not found.' });
return res.status(200).json(product);
} catch (error) {
if (error.name === 'CastError') {
return res.status(400).json({ message: 'Invalid product id.' });
}
return res.status(500).json({ message: 'Unable to update product.' });
}
}
Only copy allowed fields from req.body into the update instead of passing the whole request body to Mongoose, and wrap the query in a try/catch block. You should use 401 for a missing or invalid login, 403 for a signed-in user without permission, 400 for invalid input (including an invalid id format), and 404 when the product does not exist. And remember not to return raw database errors to the browser.
Activity Instructions
Answer the following questions:
- Why wait for the API response before changing the list?
Click to expand the example answer
The API may reject the request because of validation, authorization, or a missing record. Waiting keeps the displayed state aligned with server data.
- Why use an HTML template?
Click to expand the example answer
Templates keep reusable markup in HTML and allow JavaScript to clone and fill elements safely without large HTML strings. They also help separate presentation code from logic.
- Why use event delegation?
Click to expand the example answer
One listener handles buttons in current and newly rendered cards, so listeners do not need to be recreated after every render.
- What is the difference between confirmation and authorization?
Click to expand the example answer
Confirmation helps prevent accidental actions; server authorization determines whether the action is actually allowed.
- What response fits a signed-in customer calling an admin-only delete route?
Click to expand the example answer
The API should return
403 Forbiddenbecause the user is authenticated but lacks the required role.
Deliverables Checklist
- You can explain how PUT and DELETE update a list without a full page refresh.
- You can use HTML templates for display cards and edit forms.
- You can explain why the API enforces validation and authorization.
- You can identify how successful and failed API responses affect the interface.
Other Links:
- Return to: Week Overview | Course Home