Exercise 3: Replacing Tags and Finding Unused Tags — Possible Solution ======================================================================== // PUT /posts/:slug/tags { "tags": ["prisma", "orm"] } app.put("/posts/:slug/tags", async (req, res) => { const raw: unknown[] = Array.isArray(req.body.tags) ? req.body.tags : []; const names = [...new Set(raw.map((t) => String(t).trim().toLowerCase()))] .filter((n) => n.length > 0); const post = await prisma.post.update({ where: { slug: req.params.slug }, data: { tags: { set: [], connectOrCreate: names.map((name) => ({ where: { name }, create: { name } })), }, }, select: { slug: true, tags: { select: { name: true } } }, }); res.json(post); }); // GET /tags/unused app.get("/tags/unused", async (req, res) => { const tags = await prisma.tag.findMany({ where: { posts: { none: {} } }, orderBy: { name: "asc" }, select: { name: true }, }); res.json(tags); }); Testing with curl: $ curl -X PUT localhost:3000/posts/hello-api/tags \ -H "Content-Type: application/json" -d '{"tags":["Prisma","orm","prisma"]}' {"slug":"hello-api","tags":[{"name":"prisma"},{"name":"orm"}]} $ curl localhost:3000/tags/unused [{"name":"api"}] # "api" was on the post before, and now isn't $ curl -X PUT localhost:3000/posts/no-such-post/tags \ -H "Content-Type: application/json" -d '{"tags":[]}' {"error":"Not found"} # P2025 turned into a 404 by the error handler WHY THIS WORKS AS AN ANSWER ------------------------------ It reuses the set + connectOrCreate pattern from Chapter 8 inside a real route, uses the none relation filter from Chapter 9, and tests both the success path and the error path. Note that set only removes links — the "api" tag itself survives, which is exactly why it then shows up as unused.