Content Moderation

intermediate*

AI-powered content moderation for user-generated content. Uses OpenAI's free moderation API for text.

moderationcontentsafetyopenaiugc
Tested on201619TS5.9
$ bunx sinew add developer-experience/content-moderation
Interactive demo coming soon

1The Problem

User-generated content needs moderation for:

  • Harmful content detection
  • Policy enforcement
  • Batch content processing
  • Configurable severity levels

2The Solution

Use OpenAI's free moderation API for text content analysis. Includes policy definitions for different content types, middleware for automatic checking, and a React hook for client-side validation.

3Files

lib/moderation/text.ts

lib/moderation/text.tsTypeScript
import OpenAI from "openai";

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

// Categories returned by the omni-moderation model. Derived from the SDK so the
// shape (including `boolean | null` for the illicit categories) stays accurate.
export type ModerationCategories = OpenAI.Moderation["categories"];
export type ModerationCategoryScores = OpenAI.Moderation["category_scores"];

export interface ModerationResult {
  flagged: boolean;
  categories: ModerationCategories;
  categoryScores: ModerationCategoryScores;
}

// Moderate text content using OpenAI
export async function moderateText(text: string): Promise<ModerationResult> {
  const response = await openai.moderations.create({
    model: "omni-moderation-latest",
    input: text,
  });
  const result = response.results[0];

  return {
    flagged: result.flagged,
    categories: result.categories,
    categoryScores: result.category_scores,
  };
}

// Check if content is safe
export async function isSafe(text: string): Promise<boolean> {
  const result = await moderateText(text);
  return !result.flagged;
}

// Get detailed violation info
export async function getViolations(text: string): Promise<{ category: string; score: number }[]> {
  const result = await moderateText(text);
  if (!result.flagged) return [];

  return Object.entries(result.categories)
    .filter(([, flagged]) => flagged)
    .map(([category]) => ({
      category,
      score: result.categoryScores[category as keyof ModerationCategoryScores],
    }))
    .sort((a, b) => b.score - a.score);
}

lib/moderation/policies.ts

lib/moderation/policies.tsTypeScript
import type { ModerationCategories, ModerationCategoryScores } from "./text";

export interface ModerationPolicy {
  name: string;
  blockedCategories: Partial<Record<keyof ModerationCategories, boolean>>;
  thresholds?: Partial<Record<keyof ModerationCategories, number>>;
  action: "block" | "review" | "warn";
}

export const policies: Record<string, ModerationPolicy> = {
  // Strict policy for public content
  strict: {
    name: "Strict",
    blockedCategories: {
      sexual: true,
      hate: true,
      harassment: true,
      "self-harm": true,
      violence: true,
    },
    action: "block",
  },

  // Moderate policy for community content
  moderate: {
    name: "Moderate",
    blockedCategories: {
      "sexual/minors": true,
      "hate/threatening": true,
      "violence/graphic": true,
    },
    thresholds: {
      sexual: 0.8,
      hate: 0.7,
      harassment: 0.7,
    },
    action: "review",
  },

  // Permissive policy for mature platforms
  permissive: {
    name: "Permissive",
    blockedCategories: {
      "sexual/minors": true,
    },
    action: "warn",
  },
};

export function checkPolicy(
  policy: ModerationPolicy,
  categories: ModerationCategories,
  scores: ModerationCategoryScores
): { passed: boolean; violations: string[]; action: ModerationPolicy["action"] | "allow" } {
  const violations: string[] = [];

  for (const [category, blocked] of Object.entries(policy.blockedCategories)) {
    if (blocked && categories[category as keyof ModerationCategories]) {
      violations.push(category);
    }
  }

  if (policy.thresholds) {
    for (const [category, threshold] of Object.entries(policy.thresholds)) {
      if (scores[category as keyof ModerationCategoryScores] >= threshold) {
        if (!violations.includes(category)) violations.push(category);
      }
    }
  }

  // Clean content is allowed; only apply the policy's action on a violation.
  return {
    passed: violations.length === 0,
    violations,
    action: violations.length > 0 ? policy.action : "allow",
  };
}

app/api/moderate/route.ts

app/api/moderate/route.tsTypeScript
import { NextRequest, NextResponse } from "next/server";
import { moderateText } from "@/lib/moderation/text";
import { policies, checkPolicy } from "@/lib/moderation/policies";

export async function POST(req: NextRequest) {
  const { text, policy: policyName = "moderate" } = await req.json();

  if (!text || typeof text !== "string") {
    return NextResponse.json({ error: "Text is required" }, { status: 400 });
  }

  const result = await moderateText(text);
  const policy = policies[policyName] || policies.moderate;
  const policyResult = checkPolicy(policy, result.categories, result.categoryScores);

  return NextResponse.json({
    flagged: result.flagged,
    passed: policyResult.passed,
    action: policyResult.action,
    violations: policyResult.violations,
    scores: result.categoryScores,
  });
}

hooks/use-moderation.ts

hooks/use-moderation.tsTypeScript
"use client";

import { useState, useCallback } from "react";

export function useModeration(policy = "moderate") {
  const [isChecking, setIsChecking] = useState(false);
  const [result, setResult] = useState<{
    passed: boolean;
    violations: string[];
  } | null>(null);

  const checkContent = useCallback(
    async (text: string) => {
      setIsChecking(true);
      try {
        const response = await fetch("/api/moderate", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ text, policy }),
        });
        const data = await response.json();
        setResult(data);
        return data;
      } finally {
        setIsChecking(false);
      }
    },
    [policy]
  );

  return { isChecking, result, checkContent };
}

4Dependencies

$ bun add openai

5Configuration

Environment Variables

| Variable | Description | Required | | ---------------- | -------------- | -------- | | OPENAI_API_KEY | OpenAI API key | Yes |

Note: The OpenAI moderation endpoint is free to use.

6Usage

Check Content Before Submission

"use client";

import { useModeration } from "@/hooks/use-moderation";

export function CommentForm() {
  const { isChecking, checkContent } = useModeration("strict");

  const handleSubmit = async (text: string) => {
    const result = await checkContent(text);

    if (!result.passed) {
      alert(`Content blocked: ${result.violations.join(", ")}`);
      return;
    }

    // Submit the content
    await submitComment(text);
  };

  return <form onSubmit={handleSubmit}>...</form>;
}
TSX

Server-Side Moderation

import { isSafe, getViolations } from "@/lib/moderation/text";

export async function createPost(content: string) {
  if (!(await isSafe(content))) {
    const violations = await getViolations(content);
    throw new Error(`Content violates policy: ${violations[0].category}`);
  }

  return db.post.create({ data: { content } });
}
TypeScript

7Alternatives

  • OpenAI Moderation - Free, good accuracy
  • Perspective API - Google's toxicity detection
  • AWS Rekognition - Image/video moderation

Related patterns