NestJS HTML queue for UGC moderation

NestJS already had a JSON UGC safety API. Ops needed a human queue. HTML admin routes wrap the same service with confirm steps.

Sander Korf2 min read
nestjsugctypescript

Creator UGC NestJS backend. UgcSafetyService already exposed a JSON moderation API. The HTML admin workbench covered campaigns and Stripe. Ops still needed a human queue for OPEN REPORT / BLOCK within about a day: hide content, suspend, dismiss, unsuspend. JSON is fine for machines. Humans want a page with a badge and a confirm step.

I added server-rendered HTML admin routes on top of the same service. GET queue lists open items. GET confirm shows what you are about to do. POST act runs the mutation, then 303 redirects back with a flash message. No SPA shell. No second source of truth.

Action enum is small on purpose: hide-suspend | hide | suspend | dismiss. An exhaustive switch maps each action to flags { hideContent, suspendUser, dismiss }. Home attention queue shows the open count badge so nobody has to dig for work. Confirm pages sit between click and mutate because Hide + Suspend is not a one-click joke.

The JSON API stayed untouched. HTML is a thin shell. Same ugcSafetyService.actOnReport. Same domain rules. Ops gets a workbench that matches how they already open campaign pages.

Moderation queue: confirm before act

Fake HTML admin row. Four actions map to flags. Confirm step blocks one-click Hide + Suspend.

Flagged comment: "this post is spam"

rep_demo_01

REPORTOPEN

Pick an action. Confirm page sits between click and mutate.

export const ADMIN_MODERATION_ACTIONS = ['hide-suspend', 'hide', 'suspend', 'dismiss'] as const
 
private moderationActInput(action: AdminModerationAction): ModerationActInput {
	switch (action) {
		case 'hide-suspend': return { hideContent: true, suspendUser: true }
		case 'hide': return { hideContent: true }
		case 'suspend': return { suspendUser: true }
		case 'dismiss': return { dismiss: true }
		default: { const _exhaustive: never = action; return _exhaustive }
	}
}
 
// POST act -> ugcSafetyService.actOnReport -> redirect 303 to /admin/moderation?msg=...

If moderators can hurt a creator with one misclick, put a confirm page in front of the service call. The API can stay JSON. The humans still need HTML.


Happy coding! Sander