Deleting Project Images
Destroying Subdocuments
Section titled “Destroying Subdocuments”If an administrator uploads the wrong picture, deleting it requires updating both the physical file system (optional but recommended) and, absolutely, the MongoDB subdocument array.
Just like with our update method, we want to perform this operation silently via an asynchronous fetch request to our backend API.
1. The Frontend DELETE Request
Section titled “1. The Frontend DELETE Request”In project-form.ejs, let’s add a secondary “Delete” button inside the metadata form we built previously, and attach a new function.
<!-- Inside partials/image-edit-card.ejs --><button type="button" class="btn btn-save" onclick="updateImageMetadata('<%= project._id %>', '<%= image._id %>')"> Save Metadata</button><button type="button" class="btn btn-danger" onclick="deleteProjectImage('<%= project._id %>', '<%= image._id %>')"> Delete Image</button>And in the script block at the bottom of the page:
// At the bottom of project-form.ejsasync function deleteProjectImage(projectId, imageId) { if (!confirm("Are you sure you want to permanently delete this image?")) return;
try { const response = await fetch( `/admin/projects/${projectId}/images/${imageId}`, { method: "DELETE", }, );
const data = await response.json();
if (data.success) { // 1. Visually remove the card from the UI document .querySelector(`.image-edit-card[data-image-id="${imageId}"]`) .remove(); } else { alert("Failed to delete image metadata."); } } catch (error) { console.error(error); alert("An unexpected error occurred."); }}2. The Backend Route & Database Operation
Section titled “2. The Backend Route & Database Operation”When the frontend sends the DELETE verb, our Express router must respond. Let’s wire the router to a ProjectOps method that actually strips the image subdocument out of the array mapping.
The Route:
router.delete("/projects/:projectId/images/:imageId", async (req, res) => { const { projectId, imageId } = req.params; const result = await _projectOps.deleteProjectImage(projectId, imageId);
// We simply inform the frontend script of the success res.json({ success: result.success, error: result.errorMessage });});The ProjectOps Method:
Mongoose gives us an incredibly easy mechanism for pulling subdocuments out of an array: .pull() based on the _id!
async deleteProjectImage(projectId, imageId) { try { const project = await Project.findById(projectId); if (!project) return { success: false, errorMessage: "Project not found." };
// Mongoose pulls the subdocument completely out of the array mapped in memory project.projectImages.pull(imageId);
// Save the array back to MongoDB await project.save();
return { success: true }; } catch (error) { return { success: false, error, errorMessage: "Failed to delete projectImage." }; }}While .pull(imageId) deletes the metadata mapping in MongoDB, the physical
.png or .jpg file remains on your server’s disk storage. Deleting the
physical file requires extracting the image.filename from the array before
you call .pull(), and passing it into Node’s fs.unlinkSync() method.
Depending on your organization’s archiving policies, leaving orphaned files on
disk isn’t always a bad thing!
⏭ Next: The Frontend Loops
Section titled “⏭ Next: The Frontend Loops”With our database fully functional—supporting Uploads, Reading, Patching Metadata, and Deletions—we are finally ready to expose these images to the public portfolio viewers.