Skip to content

Deleting Project Images

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.

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.ejs
async 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.");
}
}

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:

routers/adminRouter.js
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!

data/projects.js
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." };
}
}
Professor Solo

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!

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.